package com.kamistoat.meimeistore.gateway.handler;

import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.kamistoat.meimeistore.commoncore.domain.R;
import com.kamistoat.meimeistore.commoncore.utils.ServletUtils;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;

/**
 * 和 sentinel结合使用回调处理器，直接在 WebExceptionHandler上进行实现，专门处理由于 Sentinel流量控制抛出的阻塞异常。
 * WebExceptionHandler 是spring响应式框架中的web异常处理基类。所有的异常处理器都是在继承该基类的基础上进行实现的。
 */
public class SentinelFallbackHandler implements WebExceptionHandler {
    /**
     * WebExceptionHandler接口唯一的实现方法。
     * ServerWebExchange: 一个响应式HTTP请求-响应交互的协议。提供响应式 ServerHttpRequest 和 ServerHttpResponse 的访问，并向服务端暴露相关属性和特性，如请求属性等。
     * <p>
     * 使用 ServerWebExchange.getRequest() / ServerWebExchange.getResponse() 可以直接获得对底层响应式请求-响应的访问
     *
     * @param serverWebExchange 全局响应式协议
     * @param throwable         传递给该 WebExceptionHandler 的异常
     * @return 非常重要，一个系统中可能存在多个实现了 WebExceptionHandler 的类，也即存在多个异常处理器，他们的优先级不同，捕获异常的顺序也就不同。
     * 每个handler通过 handle() 的返回值来告知spring异常是否被处理完成。
     * 如果返回 Mono<Void>.error，则说明该异常无法被当前handler处理，那么spring继续抛出该异常，由低优先级的handler捕获后继续处理。
     * 如果返回 Mono<Void>.success，则说明该异常已被当前handler处理完毕，请求应立即结束。但如何确定返回客户端的响应内容呢？这就是通过 ServerWebExchange 访问响应并设置内容，并不是返回Mono<Void>
     */
    @Override
    public Mono<Void> handle(ServerWebExchange serverWebExchange, Throwable throwable) {
        // P.S. 这里是HTTP的概念，不是分布式事务中的Committed概念
        // ServerHttpResponse 和 HttpServletResponse 一样。
        // 在 AbstractServerHttpResponse 类中可以看到，服务端的Response有四种状态:
        // NEW(新建)、COMMITTING(正在将响应头Headers返回给客户端)、COMMITTED(已经将响应头Headers返回给客户端，并不关心响应体是否返回成功)、COMMIT_ACTION_FAILED(响应头返回失败)
        // 若响应处于 COMMITTING / COMMITTED，则 ServerHttpResponse.isCommitted()返回 true
        if (serverWebExchange.getResponse().isCommitted()) {
            // 则当前handler不处理该异常，返回Mono.error
            return Mono.error(throwable);
        }

        // 判断异常是否是由于 Sentinel流量控制抛出的阻塞异常。如果不是，同样不由该handler处理，返回Mono.error
        // BlockException是 Sentinel专有的异常类
        // BlockException.isBlockException(Throwable) 用于判断一个异常是否是 BlockException 或其子类
        if (!BlockException.isBlockException(throwable)) {
            return Mono.error(throwable);
        }

        // 使用 Sentinel库类处理Sentinel阻塞异常，先拿到 Mono<ServerResponse>
        return handleBlockedRequest(serverWebExchange, throwable)
                // Mono.flatMap / Flux.flatMap 就和 Stream.map 一样，对流进行操作呗。只不过 Mono 中最多有1个元素，Flux中可以任意多
                // 对 Mono<ServerResponse> 中的元素调用 writeResponse() 方法。
                // 其实就是在 serverWebExchange中的ServerResponse中保存Sentinel库类的处理结果，因为只有serverWebExchange才能向外传递，这个handle()方法本身只能返回Mono<Void>
                .flatMap(response -> writeResponse(response, serverWebExchange));
    }

    /**
     * 处理由于 Sentinel流量控制抛出的阻塞异常
     *
     * @param serverWebExchange 全局响应式协议
     * @param throwable         在本类中，专指由于 Sentinel流量控制抛出的阻塞异常。
     * @return 返回 Mono<ServerResponse>。内部的 ServerResponse并不是 ServerWebExchange中的响应，后面会将其保存到 ServerWebExchange中传递出去
     */
    private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
        // 下方同样是 Sentinel专有的Manager类。获取Sentinel专有的阻塞异常handler，将阻塞异常的信息提取出来，制作成 Mono<ServerResponse>
        // 如果去看默认的 DefaultBlockRequestHandler很清楚就能理解，ServerWebExchange中的ServerResponse并没有改变，返回的 Mono<ServerResponse>是新建的Mono
        // 而 Sentinel的处理逻辑其实也很简单，就是返回一个 状态为HttpStatus.TOO_MANY_REQUESTS的response
        return GatewayCallbackManager.getBlockHandler().handleRequest(serverWebExchange, throwable);
    }

    /**
     * 将 Sentinel库类handler的处理结果，保存到可以向外部传递的 ServerWebExchange中的ServerResponse中
     *
     * @param serverResponse    在本类中，专指经过 Sentinel专有BlockHandler()处理的阻塞异常的结果。
     *                          实际上就是一个 状态为HttpStatus.TOO_MANY_REQUESTS的response
     * @param serverWebExchange 全局响应式协议。
     * @return 返回一个 Mono<Void> 用于显示当前方法的执行是否成功
     */
    private Mono<Void> writeResponse(ServerResponse serverResponse, ServerWebExchange serverWebExchange) {
        // 非常简单了就，就是将Sentinel的处理response 保存到 全局ServerWebExchange中的response中
        return ServletUtils.webFluxResponseWriter(serverWebExchange.getResponse(),
                serverResponse.statusCode(),
                "请求超过最大数，请稍后再试(TOO MANY REQUEST,THREW BY SENTINEL)",
                R.FAIL);
    }
}
